package de.jmda.core.mproc;

import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;


public abstract class RelationAnalyser
{
	/**
	 * Base class for types that describe relations between Java elements. Such
	 * relations always have a {@link #target}.
	 *
	 * @author roger@jmda.de
	 */
	public static abstract class RelationInfo
	{
		private TypeElement target;

		public RelationInfo(TypeElement target)
		{
			super();
			this.target = target;
		}

		public TypeElement getTarget()
    {
    	return target;
    }

		@Override
		public String toString()
		{
			return getClass().getSimpleName() + ": target [" + target + "]";
		}
	}
	
	/**
	 * Class describing an "is a" relation. "Is a" relations always have a {@link
	 * #source}.
	 *
	 * @author roger@jmda.de
	 */
	public static class RelationInfoIsA extends RelationInfo
	{
		private TypeElement source;

		public RelationInfoIsA(TypeElement source, TypeElement target)
		{
			super(target);
			this.source = source;
		}

		public TypeElement getSource()
    {
    	return source;
    }

		@Override
		public String toString()
		{
			return super.toString() + " source [" + source + "]";
		}
	}
	
	/**
	 * Base class for types describing a "has" relation. "Has" relations always
	 * have a {@link #source}.
	 *
	 * @author roger@jmda.de
	 */
	public static abstract class RelationInfoHas extends RelationInfo
	{
		private VariableElement source;

		public RelationInfoHas(VariableElement source, TypeElement target)
    {
	    super(target);
	    this.source = source;
    }

		public VariableElement getSource()
    {
    	return source;
    }

		@Override
		public String toString()
		{
			return
					super.toString() + ", source [" +
					source.getEnclosingElement().toString() + "." +
					source + "]";
		}
	}

	/**
	 * Class describing a "has one" relation.
	 *
	 * @author roger@jmda.de
	 */
	public static class RelationInfoHasOne extends RelationInfoHas
	{
		public RelationInfoHasOne(VariableElement source, TypeElement target)
    {
	    super(source, target);
    }
	}

	/**
	 * Class describing a "has many" relation.
	 *
	 * @author roger@jmda.de
	 */
	public static class RelationInfoHasMany extends RelationInfoHas
	{
		public RelationInfoHasMany(VariableElement source, TypeElement target)
    {
	    super(source, target);
    }
	}

	public static RelationInfoIsA calculateRelationInfoIsA(
			TypeElement source, TypeElement target)
	{
		if (ProcessingUtilities.isSupertype(target, source))
		{
			return new RelationInfoIsA(source, target);
		}

		return null;
	}

	public static RelationInfoIsA calculateRelationInfoIsADirect(
			TypeElement source, TypeElement target)
	{
		if (ProcessingUtilities.isSupertypeDirect(target, source))
		{
			return new RelationInfoIsA(source, target);
		}

		return null;
	}

	public static RelationInfoHas[] calculateRelationInfoHas(
			TypeElement source, TypeElement target)
	{
		Set<RelationInfoHas> result = new HashSet<RelationInfoHas>();

		// check each field of source against target
		List<VariableElement> fields = ProcessingUtilities.getFields(source);
		
		for (VariableElement field : fields)
		{
			TypeMirror typeOfField = field.asType();
			TypeMirror typeOfTarget = target.asType();

			if (ProcessingUtilities.isSameType(typeOfField, typeOfTarget))
			{
				result.add(new RelationInfoHasOne(field, target));
			}
			else if (typeOfField instanceof ArrayType)
			{
				ArrayType arrayTypeOfField = (ArrayType) typeOfField;
				
				if (ProcessingUtilities.isSameType(
						arrayTypeOfField.getComponentType(), typeOfTarget))
				{
					result.add(new RelationInfoHasMany(field, target));
				}
			}
			else
			{
				// try to create a declared type for the type mirror of the field
				DeclaredType declaredTypeOfField =
						ProcessingUtilities.asDeclaredType(typeOfField);

				if (declaredTypeOfField != null)
				{
					List<? extends TypeMirror> typeArgumentsOfField =
							declaredTypeOfField.getTypeArguments();
					
					if (typeArgumentsOfField.size() == 1)
					{
						// type of field is a generic type with one type argument
						TypeMirror typeArgumentOfField =
								typeArgumentsOfField.iterator().next();

						if (ProcessingUtilities.isSameType(typeArgumentOfField,
						                                   typeOfTarget))
						{
							// check if type of field is a Collection implementation
							// calculate non generic part (outer type name) of type name
							String typeOfFieldAsString = typeOfField.toString();
							int index = typeOfFieldAsString.indexOf('<');
							String outerTypeName = typeOfFieldAsString.substring(0, index);
							
							// create type from outer type name
							Class<?> outerType;

							try
	            {
		            outerType = Class.forName(outerTypeName);

		            // outer type creation successful
		            if (Collection.class.isAssignableFrom(outerType))
								{
									TypeMirror typeOfTypeArgument =
											typeArgumentsOfField.iterator().next();
									result.add(
											new RelationInfoHasMany(
													field,
													ProcessingUtilities.asTypeElement(
															typeOfTypeArgument)));
								}
							}
							catch (ClassNotFoundException e)
							{
								throw new IllegalStateException(
								    "failure finding class for name [" + outerTypeName + "]", e);
							}
						}
					}
				}
			}
		}

		return result.toArray(new RelationInfoHas[] {});
	}
}